import { ofetch, type ResponseType as OfetchResponseType } from 'ofetch';

import { createSseClient } from '../../client-core/bundle/serverSentEvents';
import type { HttpMethod } from '../../client-core/bundle/types';
import { getValidRequestBody } from '../../client-core/bundle/utils';
import type {
  Client,
  Config,
  RequestOptions,
  ResolvedRequestOptions,
} from './types';
import {
  buildOfetchOptions,
  buildUrl,
  createConfig,
  createInterceptors,
  isRepeatableBody,
  mapParseAsToResponseType,
  mergeConfigs,
  mergeHeaders,
  parseError,
  parseSuccess,
  setAuthParams,
  wrapDataReturn,
  wrapErrorReturn,
} from './utils';

type ReqInit = Omit<RequestInit, 'body' | 'headers'> & {
  body?: BodyInit | null | undefined;
  headers: ReturnType<typeof mergeHeaders>;
};

export const createClient = (config: Config = {}): Client => {
  let _config = mergeConfigs(createConfig(), config);

  const getConfig = (): Config => ({ ..._config });

  const setConfig = (config: Config): Config => {
    _config = mergeConfigs(_config, config);
    return getConfig();
  };

  const interceptors = createInterceptors<
    Request,
    Response,
    unknown,
    ResolvedRequestOptions
  >();

  // precompute serialized / network body
  const resolveOptions = async (options: RequestOptions) => {
    const opts = {
      ..._config,
      ...options,
      headers: mergeHeaders(_config.headers, options.headers),
      serializedBody: undefined,
    };

    if (opts.security) {
      await setAuthParams({
        ...opts,
        security: opts.security,
      });
    }

    if (opts.requestValidator) {
      await opts.requestValidator(opts);
    }

    if (opts.body !== undefined && opts.bodySerializer) {
      opts.serializedBody = opts.bodySerializer(opts.body);
    }

    // remove Content-Type if body is empty to avoid invalid requests
    if (opts.body === undefined || opts.serializedBody === '') {
      opts.headers.delete('Content-Type');
    }

    // if a raw body is provided (no serializer), adjust Content-Type only when it
    // equals the default JSON value to better match the concrete body type
    if (
      opts.body !== undefined &&
      opts.bodySerializer === null &&
      (opts.headers.get('Content-Type') || '').toLowerCase() ===
        'application/json'
    ) {
      const b: unknown = opts.body;
      if (typeof FormData !== 'undefined' && b instanceof FormData) {
        // let the runtime set the multipart boundary
        opts.headers.delete('Content-Type');
      } else if (
        typeof URLSearchParams !== 'undefined' &&
        b instanceof URLSearchParams
      ) {
        // standard urlencoded content type (+ charset)
        opts.headers.set(
          'Content-Type',
          'application/x-www-form-urlencoded;charset=UTF-8',
        );
      } else if (typeof Blob !== 'undefined' && b instanceof Blob) {
        const t = b.type?.trim();
        if (t) {
          opts.headers.set('Content-Type', t);
        } else {
          // unknown blob type: avoid sending a misleading JSON header
          opts.headers.delete('Content-Type');
        }
      }
    }

    // precompute network body (stability for retries and interceptors)
    const networkBody = getValidRequestBody(opts) as
      | RequestInit['body']
      | null
      | undefined;

    const url = buildUrl(opts);

    return { networkBody, opts, url };
  };

  // apply request interceptors and mirror header/method/signal back to opts
  const applyRequestInterceptors = async (
    request: Request,
    opts: ResolvedRequestOptions,
    body: BodyInit | null | undefined,
  ) => {
    for (const fn of interceptors.request.fns) {
      if (fn) {
        request = await fn(request, opts);
      }
    }
    // reflect interceptor changes into opts used by the network layer
    opts.headers = request.headers;
    opts.method = request.method as Uppercase<HttpMethod>;
    // ignore request.body changes to avoid turning serialized bodies into streams
    // body comes only from getValidRequestBody(options)
    // reflect signal if present
    opts.signal = (request as any).signal as AbortSignal | undefined;

    // When body is FormData, remove Content-Type header to avoid boundary mismatch.
    // Note: We already delete Content-Type in resolveOptions for FormData, but the
    // Request constructor (line 175) re-adds it with an auto-generated boundary.
    // Since we pass the original FormData (not the Request's body) to ofetch, and
    // ofetch will generate its own boundary, we must remove the Request's Content-Type
    // to let ofetch set the correct one. Otherwise the boundary in the header won't
    // match the boundary in the actual multipart body sent by ofetch.
    if (typeof FormData !== 'undefined' && body instanceof FormData) {
      opts.headers.delete('Content-Type');
    }

    return request;
  };

  // build ofetch options with stable retry logic based on body repeatability
  const buildNetworkOptions = (
    opts: ResolvedRequestOptions,
    body: BodyInit | null | undefined,
    responseType: OfetchResponseType | undefined,
  ) => {
    const effectiveRetry = isRepeatableBody(body)
      ? (opts.retry as any)
      : (0 as any);
    return buildOfetchOptions(opts, body, responseType, effectiveRetry);
  };

  const request: Client['request'] = async (options) => {
    const {
      networkBody: initialNetworkBody,
      opts,
      url,
    } = await resolveOptions(options as any);
    // map parseAs -> ofetch responseType once per request
    const ofetchResponseType: OfetchResponseType | undefined =
      mapParseAsToResponseType(opts.parseAs, opts.responseType);

    const $ofetch = opts.ofetch ?? ofetch;

    // create Request before network to run middleware consistently
    const networkBody = initialNetworkBody;
    const requestInit: ReqInit = {
      body: networkBody,
      headers: opts.headers as Headers,
      method: opts.method,
      redirect: 'follow',
      signal: opts.signal,
    };
    let request = new Request(url, requestInit);

    request = await applyRequestInterceptors(request, opts, networkBody);
    const finalUrl = request.url;

    // build ofetch options and perform the request (.raw keeps the Response)
    const responseOptions = buildNetworkOptions(
      opts as ResolvedRequestOptions,
      networkBody,
      ofetchResponseType,
    );

    let response = await $ofetch.raw(finalUrl, responseOptions);

    for (const fn of interceptors.response.fns) {
      if (fn) {
        response = await fn(response, request, opts);
      }
    }

    const result = { request, response };

    if (response.ok) {
      const data = await parseSuccess(response, opts, ofetchResponseType);
      return wrapDataReturn(data, result, opts.responseStyle);
    }

    let finalError = await parseError(response);

    for (const fn of interceptors.error.fns) {
      if (fn) {
        finalError = await fn(finalError, response, request, opts);
      }
    }

    // ensure error is never undefined after interceptors
    finalError = (finalError as any) || ({} as string);

    if (opts.throwOnError) {
      throw finalError;
    }

    return wrapErrorReturn(finalError, result, opts.responseStyle) as any;
  };

  const makeMethodFn =
    (method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
      request({ ...options, method } as any);

  const makeSseFn =
    (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
      const { networkBody, opts, url } = await resolveOptions(options);
      const optsForSse: any = { ...opts };
      delete optsForSse.body; // body is provided via serializedBody below
      return createSseClient({
        ...optsForSse,
        fetch: opts.fetch,
        headers: opts.headers as Headers,
        method,
        onRequest: async (url, init) => {
          let request = new Request(url, init);
          request = await applyRequestInterceptors(request, opts, networkBody);
          return request;
        },
        serializedBody: networkBody as BodyInit | null | undefined,
        signal: opts.signal,
        url,
      });
    };

  return {
    buildUrl,
    connect: makeMethodFn('CONNECT'),
    delete: makeMethodFn('DELETE'),
    get: makeMethodFn('GET'),
    getConfig,
    head: makeMethodFn('HEAD'),
    interceptors,
    options: makeMethodFn('OPTIONS'),
    patch: makeMethodFn('PATCH'),
    post: makeMethodFn('POST'),
    put: makeMethodFn('PUT'),
    request,
    setConfig,
    sse: {
      connect: makeSseFn('CONNECT'),
      delete: makeSseFn('DELETE'),
      get: makeSseFn('GET'),
      head: makeSseFn('HEAD'),
      options: makeSseFn('OPTIONS'),
      patch: makeSseFn('PATCH'),
      post: makeSseFn('POST'),
      put: makeSseFn('PUT'),
      trace: makeSseFn('TRACE'),
    },
    trace: makeMethodFn('TRACE'),
  } as Client;
};
